Skip to content

Windows support: WSL boundary, working agent dispatch, doctor, sandbox badge, and CI that would have caught it - #465

Merged
currentbits merged 25 commits into
mainfrom
coder/windows-support-hardening-wsl-boundary-d-d3d627
Aug 18, 2026
Merged

currentbits merged 25 commits into
mainfrom
coder/windows-support-hardening-wsl-boundary-d-d3d627

Conversation

@currentbits

Copy link
Copy Markdown
Owner

Closes #397, #434, #435, #436, #437, #441, #442, #448, #449, #450.

Why

#397 asked to harden Windows support. Partway through it became clear there was no working baseline to harden: isBinAvailable shelled out to which, which does not exist on Windows, so assertProviderBinary threw for every provider and no agent could be dispatched at all. The window opened, the UI worked, and the app fell over on the first run.

That bug survived because nothing in CI had ever started the app on Windows. So this PR does the hardening and removes the conditions that hid the problem.

What changed

The WSL boundary (#397, #434). On Windows a repo lives either on a Windows drive or inside a WSL distro, reachable as \\wsl$\<distro>\.... Crossing that boundary per file op is the main source of slow git and broken watching. The rule is that git, watchers and agents run on the same side as the worktree. Rather than build a second mechanism, this extends ssh.js's wrapCommand — already the single wrap seam for ssh remotes — with a WSL branch producing wsl.exe -d <distro> --cd <path> -- <argv>. runner.js's three scattered remoteHost checks collapse into one crossesBoundary(). Worktrees for a WSL-side repo now land inside the distro rather than under Windows userData, where git worktree add running in the distro could not even address them.

Agent dispatch (#442). which → where on win32, taking the first line since where prints every match; the lookup cache is now keyed by platform so a win32 miss cannot poison a posix hit. The six sites that spawn a provider binary go through cross-spawn, because Windows installs these CLIs as .cmd shims that Node refuses to exec — and the prompt travels in argv, so hand-rolled cmd.exe quoting would be a correctness and injection risk. No WSL special-casing was needed here: a project across a boundary already returns early, since the CLI lives on the other side.

Remaining POSIX assumptions (#448). verify.js uses Git Bash (bash -c) on win32 rather than cmd.exe or PowerShell, because verify commands are POSIX and the alternatives would silently mis-parse them; a missing bash is a real error, not a fallback. pathEnv.js is an explicit win32 no-op and no longer splits PATH on :, which would have shredded C:\Windows\system32 — silent corruption, not a crash. devservers.js handles npm.cmd and kills by pid on win32.

Windows doctor (#435). On project add, four advisory probes — core.longpaths, POSIX shell, Node 22, and which side of the boundary the repo is on — routed through the wrap seam so a WSL-side repo is probed inside the distro. Never blocks the add; no-op off win32.

Per-thread sandbox badge (#436). Confinement depends on agent × repo location, so it is resolved per thread. Only Codex comes out sandboxed (its own default CLI sandbox); everyone else is an honest no, with the reason on hover.

CI (#437, #449, #450). windows-latest added to the test matrix. A separate smoke job runs electron/smoke.js — a real Electron boot with real IPC and real spawn paths — on macOS and Windows, so a boot failure reads as a boot failure. One shared writeFakeBin replaces 32 local shebang writers, taking the Windows leg from 44 tests to 238.

#441, a literal NUL byte in providers.js, is fixed here too: it sat in the very cache key this PR rewrites, and it had made every plain grep of that file silently return nothing.

Verification

macOS: electron 1253/1253, renderer 937/937, memory 173/173, core 38/38, typecheck clean, smoke boots and exits 0 (and exits 1 when broken — I checked, since a smoke job that cannot fail is worthless).

Windows itself is unverified. Nothing on a Mac can prove it; the new smoke job answers that on this PR's first CI run. I would expect the display on the runner and git.exe in the worktree tests to be the first things to shake out.

Not included

Willem and others added 25 commits August 18, 2026 08:50
git, watchers and agents must run on the same side of the WSL boundary as
the worktree. A repo reached over \\wsl$ from Windows is ~10x slower under
Windows git and drops inotify events entirely.

electron/wsl.js detects which side a project path lives on; ssh.js's
wrapCommand — already the single wrap seam for ssh remotes — grows a second
branch that turns a WSL-side project into `wsl.exe -d <distro> --cd <path>
--  <argv>`. runner.js's resolveSpawn and the two remoteHost branches around
it move to one crossesBoundary() predicate so ssh and WSL stay in step.

Pure detection, no spawning, platform injectable — the suite runs the win32
cases on macOS. Off win32 every path is "unix" and nothing changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…into coder/fork-windows-support-hardening-wsl-bound-e59f7d
…into coder/fork-windows-support-hardening-wsl-bound-a11a21
…into coder/fork-windows-support-hardening-wsl-bound-ab4875
…into coder/fork-windows-support-hardening-wsl-bound-93db45
Show whether the next run is actually confined (agent × repo location)
as a header badge. The resolver reports what we pass today: only Codex
is sandboxed (its default CLI sandbox); everyone else is an honest no.
On win32, projects.add runs four advisory checks (core.longpaths,
Git Bash, Node 22, WSL boundary) through the wrap seam and returns
them on the project. Failed checks never reject the add. Off win32
the doctor is a no-op.

The add-project modal lists what's wrong and what to do; Continue
dismisses it.
Add windows-latest to the test job so win32 paths stay honest. Make
npm scripts portable ( --prefix, --import for CODER_GROK_MCP_DISABLE )
instead of pinning the Windows job to bash.

Electron on win32 only runs the WSL contract: the rest of that suite
spawns shebang files as CODER_*_BIN, and CreateProcess will not
execute them. There is no node-pty in the repo, so a ConPTY rebuild
step would be a no-op.

Winget publishes an installer. package-cross.sh produces an unsigned
portable zip, not one — do not add a manifest that points at it.
…into coder/windows-support-hardening-wsl-boundary-d-d3d627
userData is a Windows path, so a WSL-side repo's worktree used to land
on the wrong filesystem — and git worktree add running inside the distro
cannot address a UNC. Park those worktrees next to the repo on ext4
(<repoParent>/.solenta/worktrees/<id>, or /tmp if the repo is on
/mnt/<drive>) and store the matching UNC so Windows-side existsSync
still works.

Git helpers now share one wrap: a WSL UNC cwd becomes wsl.exe --cd, and
UNC path args (worktree add/remove) are rewritten to the linux path.
Non-win32 behaviour is unchanged. No file watcher exists today;
status/diff are on-demand and PR refresh is already interval-polled.
Sequentially they stack three PROBE_TIMEOUT_MS waits, so a box with no
bash and no node froze the add dialog for 15s — on exactly the platform
the doctor exists for. The probes are independent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…into coder/windows-support-hardening-wsl-boundary-d-d3d627
…into coder/windows-support-hardening-wsl-boundary-d-d3d627
The win32 file list was written before the doctor and sandbox suites
existed — parallel workers could not see each other. All three new files
are pure resolvers or ssh.setExecFile fakes, so they run on Windows, and
they are precisely the win32 logic the matrix exists to keep honest.
Windows now runs 44 tests instead of 7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two unrelated-looking bugs in the same function.

1. isBinAvailable shelled out to `which`, which does not exist on Windows,
   so every provider failed "Provider binary not found" and nothing could
   be dispatched. win32 uses `where`, which prints every match in PATH
   order — take the first line. Only the local host is in question here:
   a project across a boundary returns early in assertProviderBinary,
   because the CLI lives on the other side. The cache key now includes the
   platform so a win32 miss cannot poison a posix hit.

2. That cache key held a raw NUL byte as its separator, which made `file`
   report providers.js as binary and made every plain `grep` of this file
   print nothing and exit 1 — indistinguishable from a real no-match. It
   has cost two sessions a wrong conclusion (#441). Now the \0 escape:
   same string, plain-text file.

Agent CLIs install on Windows as .cmd shims and Node refuses to exec
those. The six sites that spawn a provider binary go through cross-spawn,
which routes them via cmd.exe with correct escaping — necessary because
the prompt travels in argv, so hand-rolled quoting would be a correctness
and injection risk.

Not yet Windows-ready: /bin/sh in verify.js, `which node` in memory-sup,
and the SHELL default in pathEnv. Nothing here proves the app boots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…into coder/fork-windows-support-hardening-wsl-bound-ea7084

# Conflicts:
#	src/components/ThreadView.tsx
…into coder/fork-windows-support-hardening-wsl-bound-4080d8

# Conflicts:
#	src/components/ThreadView.tsx
…into coder/fork-windows-support-hardening-wsl-bound-89b04f

# Conflicts:
#	src/components/ThreadView.tsx
Follow-up to #442. The four leftover POSIX-only spots now have a win32
path or an honest no-op, all testable on macOS by injecting the platform.

1. verify.js: Windows has no /bin/sh. Spawn Git Bash (`bash -c`) — the
   same POSIX shell the doctor already probes for. Verify commands are
   POSIX (`>&2`, `exit 3`); cmd.exe or PowerShell would silently
   mis-parse them. Missing bash is a real spawn error, not a fallback.
   A WSL-side project is wrapped through ssh.js so the command runs
   inside the distro. SSH remotes are left unwrapped: wrapping them
   would change macOS remote-verify behaviour.

2. memory-sup.js: `which node` is now providers.defaultWhich, so win32
   gets `where`. Two call sites, one existing helper; no new abstraction.

3. pathEnv.js: there is no login shell of the macOS/launchd kind on
   Windows. captureLoginPath and enrichProcessPath are explicit no-ops
   (source: "win32") instead of spawning /bin/zsh and failing closed.
   Also: do not split PATH on ':'. That would shred `C:\Windows\system32`.

4. devservers.js never used /bin/sh (argv is `npm run <script>`). It
   now goes through cross-spawn so npm.cmd works, wraps WSL-side roots
   the same way as verify, and kills the pid directly on win32 (no
   POSIX process group).

wrapCommand accepts an optional platform so WSL wrapping is testable
off Windows. Non-win32 behaviour is unchanged.
Nothing in CI has ever started the app on Windows, which is how a dead
dispatch path survived. smoke.js is already a full boot + IPC + fake-CLI
harness; it just could not run on win32.

resolveNodeBinary() now uses defaultWhich (where on win32, which
elsewhere) — same helper as #442. Fake CLIs stay shebang scripts so the
macOS path is unchanged. On win32 a sibling .cmd runs `node <script> %*`
and that path is what CODER_*_BIN points at, because CreateProcess will
not execute a shebang file and CODER_*_BIN must stay one executable
token (cross-spawn can launch the .cmd).

A new `smoke` job, separate from `npm test`, runs on macos-latest and
windows-latest after vite + core builds. npm run test:smoke is
`electron electron/smoke.js`. fail() already called app.exit(1); a
top-level uncaughtException handler was added because Electron stays
alive after "App threw an error during load", which would hang CI.

macOS: all five passes, exit 0. Missing dist: exit 1. Windows is
unverified until this job runs in CI.
…into coder/windows-support-hardening-wsl-boundary-d-d3d627
One writeFakeBin helper replaces 32 local shebang writers. POSIX still
writes the same #!/usr/bin/env node file. Win32 adds a .cmd wrapper
that cross-spawn can launch. The windows-latest list grows from 4
files / 44 tests to 21 / 238. gh and fm stay POSIX-only: they still
execFile the fake, which cannot run .cmd.
…into coder/windows-support-hardening-wsl-boundary-d-d3d627
Parallel workers landed one in electron/smoke.js and one in
electron/test/support/fakeBin.js. Same name, opposite rule for which
node the .cmd wrapper invokes: the test suite runs under plain node so
process.execPath is right there, smoke runs under Electron where it is
electron.exe and would boot a second Electron per fake. Both correct,
neither safe to copy into the other. Say so in both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two conflicts, both from the context-ring work (#317) landing on main
while this branch was open:

- ThreadView.tsx: git aligned the new SandboxBadge (#436) against main's
  rewritten ContextRingBadge. Both are kept — main's version of the ring
  already appears in full further down, so its side of the hunk was a
  duplicate.
- context-usage.test.js: main still had the local shebang writer; this
  branch migrated it to the shared writeFakeBin helper (#450).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@currentbits
currentbits merged commit e812519 into main Aug 18, 2026
2 of 4 checks passed
@currentbits
currentbits deleted the coder/windows-support-hardening-wsl-boundary-d-d3d627 branch October 3, 2026 16:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows support hardening: WSL boundary, doctor checks, sandbox matrix

1 participant